"""
@File       :wechat_api.py
@Time       :2023/3/6 22:26
@Author     :yaoruiqi
@function   ：
"""
from json import loads, dumps
from typing import Union

import httpx
from asgiref.sync import sync_to_async
from django.core.cache import cache
from django.db import transaction
from loguru import logger

from app_management.models import TemplateInfo


class AccessTokenManager:

    @staticmethod
    async def async_flush_access_token(app_id: str, secret: str, remark_id: str = None):
        """
        刷新并获取access_token
        官方文档：https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html
        @param app_id: 小程序或公众号app_id
        @param secret: 小程序或公众号app_secret
        @param remark_id: 标识id
        @return:
        """
        retry_times = 3
        for i in range(retry_times):
            try:
                async with httpx.AsyncClient() as client:
                    url = 'https://api.weixin.qq.com/cgi-bin/token'
                    param = {'grant_type': 'client_credential', 'appid': app_id, 'secret': secret}
                    response = await client.get(url, params=param, timeout=10)
                    response = response.json()
                    access_token = response.get('access_token')
                    assert access_token, f'请求access_token失败  response: {response}'
                return access_token, remark_id
            except AssertionError as e:
                logger.error(f'{e}')
                return False, remark_id
            except Exception as e:
                if i == retry_times - 1:
                    logger.exception(f'刷新access_token并且存入缓存发生错误  cause:{e}')
                    return False, remark_id


class TemplateMessage:

    @staticmethod
    async def async_send_tempalte_massage(access_token: str, user_open_id: str, template_name: str, **kwargs):
        """
        异步发送模版信息
        官方文档：https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Template_Message_Interface.html#发送模板消息
        @param access_token: 服务的access_token
        @param user_open_id: 用户open_id
        @param template_name: 模版名称
        @param kwargs: 模版请求数据
        """
        retry_times = 3
        for i in range(retry_times):
            try:
                template_id, template_data = await sync_to_async(template_management.get_template_info)(template_name)

                # 生成模版data参数
                for field_name in template_data:
                    field_data = kwargs.get(field_name)
                    assert field_data is not None, f'模板数据生成失败  template_name: {template_name}  lack_field: {field_name}'
                    template_data[field_name]['value'] = field_data

                # 发生请求
                async with httpx.AsyncClient() as client:
                    url = f'https://api.weixin.qq.com/cgi-bin/message/template/send?access_token={access_token}'
                    data = {
                        'access_token': access_token,
                        'touser': user_open_id,
                        'template_id': template_id,
                        'data': template_data
                    }
                    response = await client.post(url, json=data, timeout=5)
                    response = response.json()
                    assert response.get('errcode') == 0, f'异步发送模版信息失败，{response}'
                    logger.success(f'异步发送模版信息成功 open_id:{user_open_id}  template_id:{template_id}')
                    return True
            except AssertionError as e:
                logger.error(f'{e}')
                return False
            except Exception as e:
                if i == retry_times - 1:
                    logger.exception(f'异步发送模版信息失败，{e}')
                    return False

    @staticmethod
    def send_template_message(access_token: str, user_open_id: str, template_name: str, **kwargs):
        """
        发送模版信息
        官方文档：https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Template_Message_Interface.html#发送模板消息
        @param access_token: 服务的access_token
        @param user_open_id: 用户open_id
        @param template_name: 模版名称
        @param kwargs: 模版请求数据
        """
        retry_times = 3
        for i in range(retry_times):
            try:
                template_id, template_data = template_management.get_template_info(template_name)

                # 生成模版data参数
                for field_name in template_data:
                    field_data = kwargs.get(field_name)
                    assert field_data is not None, f'模板数据生成失败  template_name: {template_name}  lack_field: {field_name}'
                    template_data[field_name]['value'] = field_data

                # 发生请求
                url = f'https://api.weixin.qq.com/cgi-bin/message/template/send?access_token={access_token}'
                data = {
                    'access_token': access_token,
                    'touser': user_open_id,
                    'template_id': template_id,
                    'data': template_data
                }
                response = httpx.post(url, json=data, timeout=5).json()
                assert response.get('errcode') == 0, f'模板信息发送失败, {response}'
                logger.success(f'模板信息发送成功 open_id:{user_open_id}  template_id:{template_id}')
                return True
            except AssertionError as e:
                logger.error(f'{e}')
                return False
            except Exception as e:
                if i == retry_times - 1:
                    logger.exception(f'模版信息发送失败，{e}')
                    return False


class GetSubscriptionAllUser:

    @staticmethod
    def get_all_user(access_token: str, next_openid: str = ''):
        """
        获取公众号所有订阅用户open_id
        官方文档：https://developers.weixin.qq.com/doc/offiaccount/User_Management/Getting_a_User_List.html
        @param: access_token: 服务的access_token
        @param: next_openid: 拉取列表的最后一个用户的OPENID，不填默认从头开始拉取
        """
        retry_times = 3
        for i in range(retry_times):
            try:
                url = f'https://api.weixin.qq.com/cgi-bin/user/get'
                param = {'access_token': access_token, 'next_openid': next_openid}
                response = httpx.get(url, params=param, timeout=10).json()
                assert not response.get('errcode', 0), f'获取公众号所有订阅用户open_id失败  {response}'
                return response
            except AssertionError as e:
                logger.error(f'{e}')
                return False
            except Exception as e:
                if i == retry_times - 1:
                    logger.exception(f'获取公众号所有订阅用户open_id失败，{e}')
                    return False


class GetSubscriptionUserInfo:

    @staticmethod
    def get_user_info(access_token: str, openid: str, lang: str = 'zh_CN'):
        """
        获取用户基本信息
        官方文档：https://developers.weixin.qq.com/doc/offiaccount/User_Management/Get_users_basic_information_UnionID.html#UinonId
        @param: access_token 服务的access_token
        @param: openid 用户open_id
        @param: lang 返回国家地区语言版本  zh_CN简体，zh_TW繁体，en英语
        """
        retry_times = 3
        for i in range(retry_times):
            try:
                url = f'https://api.weixin.qq.com/cgi-bin/user/info'
                param = {'access_token': access_token, 'openid': openid, 'lang': lang}
                response = httpx.get(url, params=param, timeout=10).json()
                assert not response.get('errcode', 0), f'获取用户基本信息失败  {response}'
                return response
            except AssertionError as e:
                logger.error(f'{e}')
                return False
            except Exception as e:
                if i == retry_times - 1:
                    logger.exception(f'获取用户基本信息失败，{e}')
                    return False

    @staticmethod
    async def async_get_user_info(access_token: str, openid: str, lang: str = 'zh_CN'):
        """
        异步获取用户基本信息
        官方文档：https://developers.weixin.qq.com/doc/offiaccount/User_Management/Get_users_basic_information_UnionID.html#UinonId
        @param: access_token 服务的access_token
        @param: openid 用户open_id
        @param: lang 返回国家地区语言版本  zh_CN简体，zh_TW繁体，en英语
        """
        retry_times = 3
        for i in range(retry_times):
            try:
                url = f'https://api.weixin.qq.com/cgi-bin/user/info'
                param = {'access_token': access_token, 'openid': openid, 'lang': lang}
                async with httpx.AsyncClient() as client:
                    response = await client.get(url, params=param, timeout=10)
                    response = response.json()
                    assert not response.get('errcode', 0), f'获取用户基本信息失败  {response}'
                    return response
            except AssertionError as e:
                logger.error(f'{e}')
                return False
            except Exception as e:
                if i == retry_times - 1:
                    logger.exception(f'获取用户基本信息失败，{e}')
                    return False


class GetMiniProgramQr:

    @staticmethod
    def get_unlimited_qr_code(
            access_token: str,
            scene: str = 'scene',
            width: int = 430,
            page_path: str = '',
            check_path: bool = False,
            is_hyaline: bool = False,
            env_version: str = 'release'
    ):
        """
        获取无限制小程序码
        @param access_token: 服务access_token
        @param scene: 最大32个可见字符，只支持数字，大小写英文以及部分特殊字符：!#$&'()*+,/:;=?@-._~
        @param page_path: 前端页面路径 默认是主页
        @param env_version: 正式版为 "release"，体验版为 "trial"，开发版为 "develop"
        @param width: 二维码的宽度，单位 px，最小 280px，最大 1280px
        @param check_path: 检查page 是否存在，为 true 时 page 必须是已经发布的小程序存在的页面
        @param is_hyaline: 是否需要透明底色
        """
        assert env_version in ('release', 'trial', 'develop'), '参数错误 env_version'
        assert 280 <= width <= 1280, '小程序码宽度需在280px到1280px之间'
        retry_times = 3
        for i in range(retry_times):
            try:
                url = f'https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token={access_token}'
                data = {
                    'scene': scene,
                    'page': page_path,
                    'check_path': check_path,
                    'env_version': env_version,
                    'width': width,
                    'is_hyaline': is_hyaline,
                }
                response = httpx.post(url, json=data, timeout=5)
                assert (status_code := response.status_code) == 200, f'无限制小程序码获取失败 code: {status_code}'
                if (content_type := response.headers.get('content-type')) == 'image/jpeg':
                    return response.content
                elif content_type == 'application/json':
                    raise AssertionError(f'无限制小程序码获取失败 response: {response.json()}')
                else:
                    raise AssertionError(f'无限制小程序码获取失败，response_text: {response.text}')
            except AssertionError as e:
                logger.error(f'{e}')
                return False
            except Exception as e:
                if i == retry_times - 1:
                    logger.exception(f'无限制小程序码获取失败，{e}')
                    return False

    @staticmethod
    async def async_get_unlimited_qr_code(
            access_token: str,
            scene: str = 'scene',
            width: int = 430,
            page_path: str = '',
            check_path: bool = False,
            is_hyaline: bool = False,
            env_version: str = 'release'
    ):
        """
                获取无限制小程序码
                @param access_token: 服务access_token
                @param scene: 最大32个可见字符，只支持数字，大小写英文以及部分特殊字符：!#$&'()*+,/:;=?@-._~
                @param page_path: 前端页面路径 默认是主页
                @param env_version: 正式版为 "release"，体验版为 "trial"，开发版为 "develop"
                @param width: 二维码的宽度，单位 px，最小 280px，最大 1280px
                @param check_path: 检查page 是否存在，为 true 时 page 必须是已经发布的小程序存在的页面
                @param is_hyaline: 是否需要透明底色
                """
        assert env_version in ('release', 'trial', 'develop'), '参数错误 env_version'
        assert 280 <= width <= 1280, '小程序码宽度需在280px到1280px之间'
        retry_times = 3
        for i in range(retry_times):
            try:
                async with httpx.AsyncClient() as client:
                    url = f'https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token={access_token}'
                    data = {
                        'scene': scene,
                        'page': page_path,
                        'check_path': check_path,
                        'env_version': env_version,
                        'width': width,
                        'is_hyaline': is_hyaline,
                    }
                    response = await client.post(url, json=data, timeout=5)
                assert (status_code := response.status_code) == 200, f'无限制小程序码获取失败 code: {status_code}'
                if (content_type := response.headers.get('content-type')) == 'image/jpeg':
                    return response.content
                elif content_type == 'application/json':
                    raise AssertionError(f'无限制小程序码获取失败，response: {response.json()}')
                else:
                    raise AssertionError(f'无限制小程序码获取失败，response_text: {response.text}')
            except AssertionError as e:
                logger.error(f'{e}')
                return False
            except Exception as e:
                if i == retry_times - 1:
                    logger.exception(f'无限制小程序码获取失败，{e}')
                    return False


class GetSubscriptionQr:

    @staticmethod
    def get_qr_code(access_token: str, qr_type: str = 'QR_STR_SCENE', expire_seconds: int = 3600, scene_int: int = 0,
                    scene_str: str = ''):
        """
        创建公众号二维码
        @param access_token: 服务access_token
        @param qr_type: QR_SCENE为临时的整型参数值，QR_STR_SCENE为临时的字符串参数值，QR_LIMIT_SCENE为永久的整型参数值，QR_LIMIT_STR_SCENE为永久的字符串参数值
        @param expire_seconds: 二维码有效时间，以秒为单位。 最大不超过2592000（即30天），此字段如果不填，则默认有效期为60秒。
        @param scene_int: 整型参数值
        @param scene_str: 符串参数值
        """
        retry_times = 3
        for i in range(retry_times):
            try:
                url = f'https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token={access_token}'
                data = {
                    'action_name': qr_type,
                    'action_info': {'scene': {}}
                }
                if qr_type in ('QR_SCENE', 'QR_STR_SCENE'):
                    data.update({"expire_seconds": expire_seconds})
                if qr_type in ('QR_SCENE', 'QR_LIMIT_SCENE'):
                    data['action_info']['scene'].update({"scene_id": scene_int})
                elif qr_type in ('QR_STR_SCENE', 'QR_LIMIT_STR_SCENE'):
                    data['action_info']['scene'].update({"scene_str": scene_str})
                else:
                    raise AssertionError('参数错误 qr_type')
                response = httpx.post(url, json=data, timeout=10).json()
                assert not response.get('errcode'), f'创建公众号二维码失败，{response}'
                ticket = response.get('ticket')
                logger.info(response)
                response = httpx.get(f'https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket={ticket}')

                if (content_type := response.headers.get('content-type')) == 'image/jpg':
                    return response.content
                elif content_type == 'application/json':
                    raise AssertionError(f'ticket换取公众号二维码失败，response: {response.json()}')
                else:
                    raise AssertionError(f'ticket换取公众号二维码失败，response_text: {response.text}')
            except AssertionError as e:
                logger.error(f'{e}')
                return False
            except Exception as e:
                if i == retry_times - 1:
                    logger.exception(f'创建公众号二维码失败，{e}')
                    return False


class TemplateManagement:

    @staticmethod
    def get_template_list(access_token: str) -> Union[bool, list]:
        """
        获取模板列表
        @param access_token: 服务access_token
        @return:
        """
        retry_times = 3
        for i in range(retry_times):
            try:
                url = f'https://api.weixin.qq.com/cgi-bin/template/get_all_private_template?access_token={access_token}'
                response = httpx.get(url, timeout=10).json()
                assert (template_list := response.get('template_list')) is not None, f'获取模板信息失败，{response}'
                return template_list
            except AssertionError as e:
                logger.error(f'{e}')
                return False
            except Exception as e:
                if i == retry_times - 1:
                    logger.exception(f'获取模板列表失败，{e}')
                    return False

    def sync_template_info(self, access_token: str):
        """ 同步公众号模板信息，并更新缓存 """
        if template_info_list := self.get_template_list(access_token):
            total_template_info_dict = {i['template_id']: i['title'] for i in template_info_list}
            total_template_id_set = set(total_template_info_dict.keys())
            had_template_info_dict = {
                i['template_id']: i['template_data']
                for i in TemplateInfo.objects.all().values('template_id', 'template_data')
            }
            had_template_id_set = set(had_template_info_dict.keys())
            lack_template_info = [
                TemplateInfo(name=total_template_info_dict[template_id], template_id=template_id)
                for template_id in total_template_id_set.difference(had_template_id_set)
            ]
            invaild_template_id_set = had_template_id_set.difference(total_template_id_set)

            with transaction.atomic():
                lack_template_info and TemplateInfo.objects.bulk_create(lack_template_info)
                invaild_template_id_set and TemplateInfo.objects.filter(
                    template_id__in=invaild_template_id_set).delete()
                cache.set('template_info', dumps({
                    template_title: {
                        'template_id': template_id, 'template_data': had_template_info_dict.get(template_id)
                    } for template_id, template_title in total_template_info_dict.items()
                }))

            logger.success('模板信息同步成功')
        else:
            logger.debug('无新增模板信息')

    def get_template_info(self, template_name: str):
        """ 获取模板id """
        if template_info := cache.get('template_info'):
            template_info_dict = loads(template_info)
        else:
            self.sync_template_info(access_token=cache.get(f'access_token:subscription_tips'))
            template_info_dict = loads(cache.get('template_info'))

        template_info = template_info_dict.get(template_name)
        assert template_info, f'未获取到模板id  template_name: {template_name}'

        template_id = template_info.get('template_id')
        template_data = template_info.get('template_data')
        assert template_data, f'获取模版参数失败  template_name: {template_name}'
        return template_id, loads(template_data)


class MenuManager:

    @staticmethod
    def get_menu(access_token: str) -> Union[bool, dict]:
        """
        获取菜单
        @param access_token: 服务access_token
        @return:
        """
        retry_times = 3
        for i in range(retry_times):
            try:
                url = f'https://api.weixin.qq.com/cgi-bin/get_current_selfmenu_info?access_token={access_token}'
                response = httpx.get(url, timeout=10).json()
                assert not response.get('errcode'), f'获取菜单失败，{response}'
                return response
            except AssertionError as e:
                logger.error(f'{e}')
                return False
            except Exception as e:
                if i == retry_times - 1:
                    logger.exception(f'获取菜单失败，{e}')
                    return False

    @staticmethod
    def create_menu(access_token: str, menu_data: dict) -> bool:
        """
        创建菜单，参数详见https://developers.weixin.qq.com/doc/offiaccount/Custom_Menus/Creating_Custom-Defined_Menu.html
        @param access_token: 服务access_token
        @param menu_data: 菜单数据
        @return:
        """
        retry_times = 3
        for i in range(retry_times):
            try:
                url = f'https://api.weixin.qq.com/cgi-bin/menu/create?access_token={access_token}'
                menu_data = dumps(menu_data, ensure_ascii=False)
                response = httpx.post(url, data=menu_data, timeout=10).json()
                assert not response.get('errcode'), f'创建菜单失败，{response}'
                logger.info(response)
                return True
            except AssertionError as e:
                logger.error(f'{e}')
                return False
            except Exception as e:
                if i == retry_times - 1:
                    logger.exception(f'创建菜单失败，{e}')
                    return False

    @staticmethod
    def delete_menu(access_token: str) -> bool:
        """
        删除菜单
        @param access_token: 服务access_token
        @return:
        """
        retry_times = 3
        for i in range(retry_times):
            try:
                url = f'https://api.weixin.qq.com/cgi-bin/menu/delete?access_token={access_token}'
                response = httpx.get(url, timeout=10).json()
                assert not response.get('errcode'), f'删除菜单失败，{response}'
                return True
            except AssertionError as e:
                logger.error(f'{e}')
                return False
            except Exception as e:
                if i == retry_times - 1:
                    logger.exception(f'删除菜单失败，{e}')
                    return False


access_token_manager = AccessTokenManager()
template_message = TemplateMessage()
get_subscription_all_user = GetSubscriptionAllUser()
get_subscription_user_info = GetSubscriptionUserInfo()
get_miniprogram_qr = GetMiniProgramQr()
get_subscription_qr = GetSubscriptionQr()
template_management = TemplateManagement()
menu_manager = MenuManager()
